#!/usr/bin/env python
# coding: utf-8
"""
异地同步备份插件 - 核心同步模块
功能：执行网站、数据库、FTP、计划任务等数据的同步

Author: yang
Version: 2.0.0
"""

import json
import os
import re
import sys
import time
from typing import Dict, List, Tuple

import panel_api
import helpr

os.chdir("/www/server/panel")
sys.path.append("class/")
import public
import panelMysql


class psync():
    _PLUGIN_PATH = "/www/server/panel/plugin/remoteback/"
    _SYNC_INFO = None
    _SYNC_PANEL = None
    _SPEED_INFO = {}
    _VHOST_PATH = '/www/server/panel/vhost'
    _INFO_FILE = None
    _SPEED_FILE = None
    _TO_PATHS = []
    _TO_FILES = []
    _TO_PORTS = []
    _REQUESTS = None
    old_state = None
    _API = None
    _API_INFO = None
    _SYNC_CONFIG = None

    def __init__(self, sync_config: Dict):
        """
        初始化同步对象
        :param sync_config: 同步配置字典
        """
        if not sync_config or 'config_name' not in sync_config:
            raise ValueError("同步配置无效：缺少 config_name")
        
        self._SYNC_CONFIG = sync_config
        self._INFO_FILE = self._PLUGIN_PATH + f"config/sync_info_{sync_config['config_name']}.json"
        self._SPEED_FILE = self._PLUGIN_PATH + f"config/speed_{sync_config['config_name']}.json"
        
        # 读取同步信息
        self._SYNC_INFO = helpr.get_config(file_name=f"sync_info_{sync_config['config_name']}")
        if not self._SYNC_INFO:
            raise ValueError(f"未找到同步配置文件: sync_info_{sync_config['config_name']}.json")
        
        # 验证同步信息格式
        if not isinstance(self._SYNC_INFO, dict):
            raise ValueError("同步配置文件格式错误，必须是字典类型")
        
        # 确保必要的键存在（即使是空数组）
        for key in ['sites', 'ftps', 'databases', 'crontab', 'paths']:
            if key not in self._SYNC_INFO:
                self._SYNC_INFO[key] = []
        
        # 初始化API
        self._API = panel_api.panel_api(sync_config)
        
        # 初始化requests会话
        import requests
        if not self._REQUESTS:
            self._REQUESTS = requests.session()
        
        helpr.write_log(f"✅ 同步对象初始化成功: {sync_config.get('title', 'Unknown')}")

    # 发生错误
    def error(self, error_msg, is_exit=False):
        """
        发生错误
        :param error_msg:
        :param is_exit:
        :return:
        """
        helpr.write_log_error("=" * 50)
        helpr.write_log_error("|-发生时间: {}".format(helpr.format_date()))
        helpr.write_log_error("|-错误信息: {}".format(error_msg))
        if is_exit:
            helpr.write_log_error("|-处理结果: 终止迁移任务")
            sys.exit(0)
        helpr.write_log_error("|-处理结果: 忽略错误, 继续执行")

    def save(self):
        """
        报错同步配置
        :return:
        """
        public.writeFile(self._INFO_FILE, json.dumps(self._SYNC_INFO))

    def state(self, stype, index, state, error=''):
        """设置状态"""
        self._SYNC_INFO[stype][index]['state'] = state
        self._SYNC_INFO[stype][index]['error'] = error
        if self._SYNC_INFO[stype][index]['state'] != 1:
            self._SYNC_INFO['speed'] += 1
        self.save()

    def run(self):
        """
        开始同步
        :return: None
        """
        try:
            start_time = time.time()
            helpr.write_log('\n' + '='*80, False)
            helpr.write_log(f"🚀 开始同步任务: {self._SYNC_CONFIG.get('title', 'Unknown')}", False)
            helpr.write_log('='*80 + '\n', False)
            
            # 检查MySQL配置
            public.CheckMyCnf()
            
            # 环境检查
            helpr.write_log('📋 步骤1/7: 检查目标服务器环境...')
            if not self.check_env():
                self.error('❌ 环境检查失败, 具体错误请查看日志', True)
                return
            
            # 检查目标面板是否有正在执行的任务
            if self._API.get_dst_task_speed():
                helpr.write_log("⚠️ 目标面板有任务正在运行, 本次同步取消")
                return
            
            # 检查PHP禁用函数
            helpr.write_log('📋 步骤2/7: 检查PHP禁用函数...')
            self.check_php_disable_functions()
            
            # 同步前置配置
            helpr.write_log('📋 步骤3/7: 同步前置配置...')
            self.sync_other()
            
            # 同步网站
            if len(self._SYNC_INFO.get('sites', [])) > 0:
                helpr.write_log(f'📋 步骤4/7: 同步网站 ({len(self._SYNC_INFO["sites"])} 个)...')
                self.sync_site()
            else:
                helpr.write_log('📋 步骤4/7: 跳过网站同步（未选择）')
            
            # 同步数据库
            if len(self._SYNC_INFO.get('databases', [])) > 0:
                helpr.write_log(f'📋 步骤5/7: 同步数据库 ({len(self._SYNC_INFO["databases"])} 个)...')
                self.sync_database()
            else:
                helpr.write_log('📋 步骤5/7: 跳过数据库同步（未选择）')
            
            # 同步FTP
            if len(self._SYNC_INFO.get('ftps', [])) > 0:
                helpr.write_log(f'📋 步骤6/7: 同步FTP ({len(self._SYNC_INFO["ftps"])} 个)...')
                self.sync_ftp()
            else:
                helpr.write_log('📋 步骤6/7: 跳过FTP同步（未选择）')
            
            # 同步计划任务
            if len(self._SYNC_INFO.get('crontab', [])) > 0:
                helpr.write_log(f'📋 步骤7/7: 同步计划任务 ({len(self._SYNC_INFO["crontab"])} 个)...')
                self.sync_crontab()
            else:
                helpr.write_log('📋 步骤7/7: 跳过计划任务同步（未选择）')
            
            # 同步自定义路径
            if len(self._SYNC_INFO.get('paths', [])) > 0:
                helpr.write_log(f'📋 额外步骤: 同步自定义路径 ({len(self._SYNC_INFO["paths"])} 个)...')
                self.sync_path()
            
            # 完成
            self.write_speed('action', 'True')
            elapsed_time = time.time() - start_time
            minutes = int(elapsed_time // 60)
            seconds = int(elapsed_time % 60)
            
            helpr.write_log('\n' + '='*80, False)
            helpr.write_log(f'✅ 所有同步任务完成！耗时: {minutes}分{seconds}秒', False)
            helpr.write_log('='*80 + '\n', False)
            
        except Exception as e:
            helpr.write_log_error(f'❌ 同步任务执行失败: {str(e)}')
            helpr.write_log_error(f'错误详情: {public.get_error_info()}')
            raise

    def sync_path(self):
        """
        迁移自定义目录
        :return:
        """
        for i in range(len(self._SYNC_INFO['paths'])):
            self.state('paths', i, 1)
            sp_msg = "|-迁移自定义目录: [{}]".format(self._SYNC_INFO['paths'][i]['path'])
            self.write_speed('action', sp_msg)
            helpr.write_log(sp_msg)
            if not self._SYNC_INFO['paths'][i]['path'] in self._TO_PATHS:
                self.send(self._SYNC_INFO['paths'][i]['path'], self._SYNC_INFO['paths'][i]['to_path'])
            helpr.write_log("=" * 50)
            self.state('paths', i, 2)

    def sync_site(self):
        """
        迁移站点
        :return: None
        """
        try:
            # 开放HTTPS端口
            self._API.send_panel('/firewall?action=AddAcceptPort', 
                                {"port": '443', "type": "port", "ps": "HTTPS"}, 30)
        except Exception as e:
            helpr.write_log(f"⚠️ 开放443端口失败: {str(e)}，继续执行")
        
        total = len(self._SYNC_INFO['sites'])
        success_count = 0
        fail_count = 0
        
        for i in range(total):
            self.old_state = self._SYNC_INFO['sites'][i]['state']
            try:
                self.state('sites', i, 1)
                site = self._SYNC_INFO['sites'][i]
                sp_msg = f"   🌐 [{i+1}/{total}] 迁移网站: {site['name']}"
                self.write_speed('action', sp_msg)
                helpr.write_log(sp_msg)
                
                site_id = site['id']
                siteInfo = public.M('sites').where('id=?', (site_id,)).field(
                    'id,name,path,ps,status,edate,addtime').find()
                
                if not siteInfo:
                    err_msg = f"❌ 指定站点 [{site['name']}] 不存在!"
                    self.state('sites', i, -1, err_msg)
                    self.error(err_msg)
                    fail_count += 1
                    continue

                # 获取站点详细信息
                siteInfo['port'] = public.M('domain').where('pid=? and name=?', 
                    (site_id, site['name'])).getField('port')
                if not siteInfo['port']: 
                    siteInfo['port'] = 80
                    
                # 获取所有域名（包括主域名和附加域名）
                all_domains = public.M('domain').where('pid=?', (site_id,)).field('name,port').select()
                siteInfo['all_domains'] = all_domains if all_domains else []
                
                # 获取附加域名（用于兼容旧逻辑）
                siteInfo['domain'] = public.M('domain').where('pid=? and name!=?', 
                    (site_id, site['name'])).field('name,port').select()
                
                siteInfo['binding'] = public.M('binding').where('pid=?', 
                    (site_id,)).field('domain,path,port').select()
                siteInfo['redirect'] = self.get_redirect(siteInfo['name'])
                siteInfo['proxy'] = self.get_proxy(siteInfo['name'])
                siteInfo['dir_auth'] = self.get_dir_auth(siteInfo['name'])
                
                # 发送站点数据
                if self.send_site(siteInfo, i):
                    self.state('sites', i, 2)
                    success_count += 1
                    helpr.write_log(f"   ✅ 网站 [{site['name']}] 同步成功")
                else:
                    fail_count += 1
                    helpr.write_log_error(f"   ❌ 网站 [{site['name']}] 同步失败")
                    
                helpr.write_log("   " + "-" * 76)
                
            except Exception as e:
                helpr.write_log_error(f"   ❌ 网站 [{site.get('name', 'Unknown')}] 同步异常: {str(e)}")
                helpr.write_log_error(f"   错误详情: {public.get_error_info()}")
                self.state('sites', i, -1, str(e))
                fail_count += 1
        
        helpr.write_log(f"\n📊 网站同步统计: 成功 {success_count} 个，失败 {fail_count} 个\n")

    def send_site(self, siteInfo, index):
        """
        发送站点文件
        :param siteInfo:
        :param index:
        :return:
        """
        if not os.path.exists(siteInfo['path']):
            err_msg = "网站根目录[{}]不存在,跳过!".format(siteInfo['path'])
            self.state('sites', index, -1, err_msg)
            self.error(err_msg)
            return False
        site_id = None
        if not self._API.check_site_exist(siteInfo['name']):
            create_tag = self.create_site(siteInfo, index)
            if not create_tag:
                return False
            # create_site 中已经处理了域名同步
        else:
            helpr.write_log(f"   ℹ️  网站 [{siteInfo['name']}] 已存在，跳过创建，检查域名同步...")
            
            # 获取目标服务器上的站点ID
            site_info_result = self._API.send_panel('/data?action=getData', {
                'table': 'sites',
                'search': siteInfo['name'],
                'limit': 1
            })
            
            if site_info_result and isinstance(site_info_result, list) and len(site_info_result) > 0:
                site_id = site_info_result[0].get('id')
                # 站点已存在，也需要同步域名
                if site_id and 'all_domains' in siteInfo and siteInfo['all_domains']:
                    helpr.write_log(f"   🌐 同步域名到已存在的站点...")
                    self.sync_domains(site_id, siteInfo['name'], siteInfo['all_domains'])

        # 准备文件和目录路径
        s_files = [
            [self._VHOST_PATH + '/nginx/{}.conf'.format(siteInfo['name']), "网站配置文件"],
            [self._VHOST_PATH + '/apache/{}.conf'.format(siteInfo['name']), "网站配置文件"],
            [self._VHOST_PATH + '/cert/{}'.format(siteInfo['name']), "网站SSL证书"],
            [self._VHOST_PATH + '/ssl/{}'.format(siteInfo['name']), "相关的证书夹"],
            [self._VHOST_PATH + '/rewrite/{}.conf'.format(siteInfo['name']), "伪静态配置"],
            [self._VHOST_PATH + '/nginx/redirect/{}'.format(siteInfo['name']), "重定向配置"],
            [self._VHOST_PATH + '/nginx/proxy/{}'.format(siteInfo['name']), "反向代理配置"],
            [self._VHOST_PATH + '/nginx/dir_auth/{}'.format(siteInfo['name']), "目录保护配置"],
            [self._VHOST_PATH + '/apache/redirect/{}'.format(siteInfo['name']), "重定向配置"],
            [self._VHOST_PATH + '/apache/proxy/{}'.format(siteInfo['name']), "反向代理配置"],
            [self._VHOST_PATH + '/apache/dir_auth/{}'.format(siteInfo['name']), "目录保护配置"],
            ["/etc/letsencrypt/live/{}".format(siteInfo['name']), "网站SSL证书"]
        ]

        # 处理日志路径
        nginx_conf = self._VHOST_PATH + '/nginx/{}.conf'.format(siteInfo['name'])
        httpd_conf = self._VHOST_PATH + '/apache/{}.conf'.format(siteInfo['name'])
        tmp = None
        
        # 读取nginx配置获取日志路径
        if os.path.exists(nginx_conf):
            ng_config = public.readFile(nginx_conf)  # 修复：原来错误地读取了 httpd_conf
            if ng_config:
                tmp = re.findall(r'access_log\s+(.{10,128});', ng_config)

        # 读取apache配置获取日志路径
        if os.path.exists(httpd_conf):
            httpd_config = public.readFile(httpd_conf)
            if httpd_config:
                tmp = re.findall(r'ErrorLog\s+"(.+)"', httpd_config)

        if tmp:
            log_path = os.path.dirname(tmp[0])
            if log_path != '/www/wwwlogs':
                self._API.send_panel('/files?action=CreateDir', {"path": log_path})

        # 删除默认创建的index.html
        self._API.send_panel('/files?action=DeleteFile', {"path": siteInfo['path'] + '/index.html'})

        # 子目录绑定伪静态
        for bind in siteInfo['binding']:
            filename = self._VHOST_PATH + '/rewrite/{}_{}.conf'.format(siteInfo['name'], bind['path'])
            if os.path.exists(filename): s_files.append([filename, "子目录绑定配置"])

        # 发送文件列表
        self.send_list(s_files)

        # 发送反向代理/重定向/目录保护配置列表
        self.send_proxy_redirect_dir_auth(siteInfo)

        # 重载nginx/httpd
        self._API.send_panel('/system?action=ServiceAdmin', {"name": self._get_stype(), "type": "reload"})

        # 迁移网站文件
        if not siteInfo['path'] in self._TO_PATHS:
            self._TO_PATHS.append(siteInfo['path'])
            if not self.send(siteInfo['path'], siteInfo['path']):
                self.state('sites', index, -1, '数据传输失败!')
                return False
        return True

    def send_proxy_redirect_dir_auth(self, siteInfo):
        """
        迁移反向代理/重定向/目录保护配置列表
        :param siteInfo:
        :return:
        """
        if siteInfo['redirect']:
            fname = "/www/server/panel/data/redirect.conf"
            result = self._API.send_panel('/files?action=GetFileBody', {"path": fname})
            if result['status']:
                data = json.loads(result['data'])
                if not data: data = []
                for n in siteInfo['redirect']:
                    data.append(n)
                result = self._API.send_panel('/files?action=SaveFileBody',
                                              {"path": fname, "data": json.dumps(data), "encoding": "utf-8"})

        if siteInfo['proxy']:
            fname = "/www/server/panel/data/proxyfile.json"
            result = self._API.send_panel('/files?action=GetFileBody', {"path": fname})
            if result['status']:
                data = json.loads(result['data'])
                if not data: data = []
                for n in siteInfo['proxy']:
                    data.append(n)
                result = self._API.send_panel('/files?action=SaveFileBody',
                                              {"path": fname, "data": json.dumps(data), "encoding": "utf-8"})

        if siteInfo['dir_auth']:
            fname = "/www/server/panel/data/site_dir_auth.json"
            result = self._API.send_panel('/files?action=GetFileBody', {"path": fname})
            if result['status']:
                data = json.loads(result['data'])
                if not data: data = {}
                data[siteInfo['name']] = siteInfo['dir_auth']
                result = self._API.send_panel('/files?action=SaveFileBody',
                                              {"path": fname, "data": json.dumps(data), "encoding": "utf-8"})

    def send_list(self, s_files):
        """
        发送文件列表
        :param s_files:
        :return:
        """
        for f in s_files:
            if not os.path.exists(f[0]): continue
            self.send(f[0], f[0], True)

    def _get_stype(self):
        """
        获取web服务器类型
        :return:
        """
        webserver = ''
        if os.path.exists('/www/server/nginx/sbin/nginx'):
            webserver = 'nginx'
        elif os.path.exists('/www/server/apache/bin/httpd'):
            webserver = 'httpd'
        elif os.path.exists('/usr/local/lsws/bin/lswsctrl'):
            webserver = 'lswsctrl'
        return webserver

    def create_site(self, siteInfo, index):
        """
        创建远程站点
        :param siteInfo:
        :param index:
        :return:
        """
        self.write_speed('done', '正在创建站点配置')
        pdata = {}
        domains = self.format_domain(siteInfo['domain'])
        pdata['webname'] = json.dumps({"domain": siteInfo['name'], "domainlist": domains, "count": len(domains)})
        pdata['ps'] = siteInfo['ps']
        pdata['path'] = siteInfo['path']
        pdata['ftp'] = 'false'
        pdata['sql'] = 'false'
        pdata['codeing'] = 'utf-8'
        pdata['type'] = 'PHP'
        pdata['version'] = '00'
        pdata['type_id'] = '0'
        pdata['port'] = siteInfo['port']
        if not pdata['port']: pdata['port'] = 80

        result = self._API.send_panel('/site?action=AddSite', pdata)
        if not 'siteStatus' in result:
            err_msg = '站点[{}]创建失败, {}'.format(siteInfo['name'], result['msg'])
            self.state('sites', index, -1, err_msg)
            self.error(err_msg)
            return False
        if not result['siteStatus']:
            err_msg = '站点[{}]创建失败!'.format(siteInfo['name'])
            self.state('sites', index, -1, err_msg)
            self.error(err_msg)
            return False
        id = result['siteId']
        
        # 同步所有域名（除了主域名，主域名在创建站点时已添加）
        if 'all_domains' in siteInfo and siteInfo['all_domains']:
            helpr.write_log(f"   🌐 开始同步域名，共 {len(siteInfo['all_domains'])} 个...")
            self.sync_domains(id, siteInfo['name'], siteInfo['all_domains'])
        
        # 修改到期时间
        if siteInfo['edate'] != '0000-00-00':
            self._API.send_panel('/site?action=SetEdate', {"id": id, "edate": siteInfo['edate']})
        # 修改站点状态
        if siteInfo['status'] == '0':
            self._API.send_panel('/site?action=SiteStop', {"id": id, "name": siteInfo['name']})

        # 设置子目录绑定
        if siteInfo['binding']:
            for binding in siteInfo['binding']:
                self._API.send_panel('/files?action=CreateDir', {"path": siteInfo['path'] + '/' + binding['path']})
                self._API.send_panel('/site?action=AddDirBinding',
                                     {"dirName": binding['path'], "domain": binding['domain'], "id": id})

        # 设置301重定向
        if siteInfo['redirect']:
            for red_info in siteInfo['redirect']:
                self._API.send_panel('/site?action=CreateRedirect', red_info)

        # 设置反向代理
        if siteInfo['proxy']:
            for proxy_info in siteInfo['proxy']:
                self._API.send_panel('/site?action=CreateProxy', proxy_info)
        return True

    def sync_domains(self, site_id, site_name, all_domains):
        """
        同步网站的所有域名到目标服务器
        :param site_id: 目标服务器上的站点ID
        :param site_name: 站点名称（主域名）
        :param all_domains: 所有域名列表 [{'name': 'domain.com', 'port': 80}, ...]
        :return: None
        """
        try:
            if not all_domains or len(all_domains) == 0:
                helpr.write_log(f"      ℹ️  无附加域名需要同步")
                return
            
            # 获取目标服务器上当前站点的域名列表
            dst_domains_result = self._API.send_panel('/data?action=getData', {
                'table': 'domain',
                'list': 'True',
                'search': ''  # 获取所有域名
            })
            
            # 筛选出属于当前站点的域名
            dst_domains_map = {}
            if dst_domains_result and isinstance(dst_domains_result, list):
                for d in dst_domains_result:
                    if d.get('pid') == site_id:
                        dst_domains_map[d.get('name')] = d
            
            added_count = 0
            skipped_count = 0
            
            # 遍历源服务器的所有域名
            for domain_info in all_domains:
                domain_name = domain_info.get('name', '').strip()
                domain_port = domain_info.get('port', 80)
                
                if not domain_name:
                    continue
                
                # 跳过主域名（创建站点时已添加）
                if domain_name == site_name:
                    helpr.write_log(f"      ✓ {domain_name}:{domain_port} (主域名，跳过)")
                    skipped_count += 1
                    continue
                
                # 检查域名是否已存在
                if domain_name in dst_domains_map:
                    helpr.write_log(f"      ✓ {domain_name}:{domain_port} (已存在)")
                    skipped_count += 1
                    continue
                
                # 添加域名到目标服务器
                helpr.write_log(f"      ➕ 添加域名: {domain_name}:{domain_port}")
                try:
                    result = self._API.send_panel('/site?action=AddDomain', {
                        'domain': domain_name,
                        'webname': site_name,
                        'id': site_id,
                        'port': str(domain_port)
                    })
                    
                    if result and isinstance(result, dict):
                        if result.get('domains'):
                            # 检查添加结果
                            for domain_result in result['domains']:
                                if domain_result.get('status'):
                                    helpr.write_log(f"         ✅ {domain_result.get('name')} 添加成功")
                                    added_count += 1
                                else:
                                    helpr.write_log_error(f"         ❌ {domain_result.get('name')} 添加失败: {domain_result.get('msg', '未知错误')}")
                        elif result.get('status'):
                            helpr.write_log(f"         ✅ {domain_name} 添加成功")
                            added_count += 1
                        else:
                            helpr.write_log_error(f"         ❌ {domain_name} 添加失败: {result.get('msg', '未知错误')}")
                    else:
                        helpr.write_log_error(f"         ⚠️ {domain_name} 响应格式异常")
                        
                except Exception as e:
                    helpr.write_log_error(f"         ❌ {domain_name} 添加异常: {str(e)}")
            
            helpr.write_log(f"   ✅ 域名同步完成 (新增: {added_count} 个，已存在: {skipped_count} 个)")
            
        except Exception as e:
            helpr.write_log_error(f"   ⚠️ 域名同步失败: {str(e)}")
            helpr.write_log_error(f"   错误详情: {public.get_error_info()}")

    def format_domain(self, domain):
        """
        格式化域名
        :param domain:
        :return:
        """
        domains = []
        for d in domain:
            domains.append("{}:{}".format(d['name'], d['port']))
        return domains

    def get_redirect(self, siteName):
        """
        取指定站点的重定向信息
        :param siteName: 站点名称
        :return: 重定向配置列表
        """
        try:
            r_file = '/www/server/panel/data/redirect.conf'
            if not os.path.exists(r_file):
                return []
            
            file_content = public.readFile(r_file)
            if not file_content:
                return []
            
            result = []
            r_data = json.loads(file_content)
            for s in r_data:
                if s.get('sitename') == siteName:
                    result.append(s)
            return result
        except json.JSONDecodeError as e:
            helpr.write_log_error(f"⚠️ 解析重定向配置失败: {str(e)}")
            return []
        except Exception as e:
            helpr.write_log_error(f"⚠️ 获取重定向配置失败: {str(e)}")
            return []

    def get_proxy(self, siteName):
        """
        取指定站点的反代配置信息
        :param siteName: 站点名称
        :return: 反代配置列表
        """
        try:
            r_file = '/www/server/panel/data/proxyfile.json'
            if not os.path.exists(r_file):
                return []
            
            file_content = public.readFile(r_file)
            if not file_content:
                return []
            
            result = []
            r_data = json.loads(file_content)
            for s in r_data:
                if s.get('sitename') == siteName:
                    result.append(s)
            return result
        except json.JSONDecodeError as e:
            helpr.write_log_error(f"⚠️ 解析反代配置失败: {str(e)}")
            return []
        except Exception as e:
            helpr.write_log_error(f"⚠️ 获取反代配置失败: {str(e)}")
            return []

    def get_dir_auth(self, siteName):
        """
        取指定站点目录保护配置信息
        :param siteName: 站点名称
        :return: 目录保护配置字典
        """
        try:
            r_file = '/www/server/panel/data/site_dir_auth.json'
            if not os.path.exists(r_file):
                return {}
            
            file_content = public.readFile(r_file)
            if not file_content:
                return {}
            
            r_data = json.loads(file_content)
            if not r_data:
                return {}
            
            return r_data.get(siteName, {})
        except json.JSONDecodeError as e:
            helpr.write_log_error(f"⚠️ 解析目录保护配置失败: {str(e)}")
            return {}
        except Exception as e:
            helpr.write_log_error(f"⚠️ 获取目录保护配置失败: {str(e)}")
            return {}

    def sync_database(self):
        """
        迁移数据库
        :return: None
        """
        total = len(self._SYNC_INFO['databases'])
        success_count = 0
        fail_count = 0
        
        for i in range(total):
            try:
                self.state('databases', i, 1)
                db = self._SYNC_INFO['databases'][i]
                sp_msg = f"   💾 [{i+1}/{total}] 迁移数据库: {db['name']}"
                self.write_speed('action', sp_msg)
                helpr.write_log(sp_msg)
                
                db_id = db['id']
                dbInfo = public.M('databases').where('id=?', (db_id,)).field(
                    'id,name,username,password,ps').find()
                
                if not dbInfo:
                    err_msg = f"❌ 指定数据库 [{db['name']}] 不存在!"
                    self.state('databases', i, -1, err_msg)
                    self.error(err_msg)
                    fail_count += 1
                    continue
                
                dbInfo['accept'] = self.GetDatabaseAccess(dbInfo['name'])
                dbInfo['character'] = self.get_database_character(dbInfo['name'])
                
                if self.send_database(dbInfo, i):
                    self.state('databases', i, 2)
                    success_count += 1
                    helpr.write_log(f"   ✅ 数据库 [{db['name']}] 同步成功")
                else:
                    fail_count += 1
                    helpr.write_log_error(f"   ❌ 数据库 [{db['name']}] 同步失败")
                    
                helpr.write_log("   " + "-" * 76)
                
            except Exception as e:
                helpr.write_log_error(f"   ❌ 数据库 [{db.get('name', 'Unknown')}] 同步异常: {str(e)}")
                helpr.write_log_error(f"   错误详情: {public.get_error_info()}")
                self.state('databases', i, -1, str(e))
                fail_count += 1
        
        helpr.write_log(f"\n📊 数据库同步统计: 成功 {success_count} 个，失败 {fail_count} 个\n")

    def get_database_character(self, db_name):
        """
        取数据库字符集
        :param db_name: 数据库名称
        :return: 字符集名称
        """
        try:
            tmp = panelMysql.panelMysql().query("show create database `%s`" % db_name.strip())
            c_type = str(re.findall(r"SET\s+([\w\d-]+)\s", tmp[0][1])[0])
            c_types = ['utf8', 'utf-8', 'gbk', 'big5', 'utf8mb4']
            if c_type.lower() not in c_types:
                helpr.write_log(f"⚠️ 数据库 [{db_name}] 字符集 [{c_type}] 不在支持列表中，使用默认 utf8")
                return 'utf8'
            return c_type
        except IndexError:
            helpr.write_log(f"⚠️ 无法解析数据库 [{db_name}] 的字符集，使用默认 utf8")
            return 'utf8'
        except Exception as e:
            helpr.write_log_error(f"⚠️ 获取数据库 [{db_name}] 字符集失败: {str(e)}，使用默认 utf8")
            return 'utf8'

    def send_database(self, dbInfo, index):
        """
        发送数据库
        :param dbInfo:
        :param index:
        :return:
        """
        if not self._API.check_db_exist(dbInfo['name']):
            # 创建远程库
            db_ret = self.create_database(dbInfo, index)
            if not db_ret:
                self.state('databases', index, -1, "远程数据库创建失败！")
                return False
        else:
            helpr.write_log("|-数据库已存在，正在更新...")
        # 导出
        filename = self.export_database(dbInfo['name'], index)
        if not filename: return False
        upload_file = '/www/backup/database/psync_import_{}.sql.gz'.format(dbInfo['name'])
        self._API.send_panel('/files?action=ExecShell', {"shell": "rm -f " + upload_file, "path": "/www"}, 30)
        # 上传
        if self.send(filename, upload_file):
            # 导入
            self.write_speed('done', '正在导入数据库')
            helpr.write_log("|-正在导入数据库{}...".format(dbInfo['name']))
            self._API.send_panel('/database?action=InputSql', {"name": dbInfo['name'], "file": upload_file}, 7300)
            self._API.send_panel('/files?action=ExecShell', {"shell": "rm -f " + upload_file, "path": "/www"}, 30)
            helpr.write_log(f"|-{dbInfo['name']} 导入完成！")
            return True
        self.state('databases', index, -1, "数据传输失败")
        return False

    def create_database(self, dbInfo, index):
        """
        创建远程数据库
        :param dbInfo:
        :param index:
        :return:
        """
        pdata = {}
        pdata['name'] = dbInfo['name']
        pdata['db_user'] = dbInfo['username']
        pdata['password'] = dbInfo['password']
        pdata['dtype'] = 'MySQL'
        pdata['dataAccess'] = dbInfo['accept']
        if dbInfo['accept'] != '%' and dbInfo['accept'] != '127.0.0.1':
            pdata['dataAccess'] = 'ip'
        pdata['address'] = dbInfo['accept']
        pdata['ps'] = dbInfo['ps']
        pdata['codeing'] = dbInfo['character']
        result = self._API.send_panel('/database?action=AddDatabase', pdata)

        if result['status']: return True
        err_msg = '数据库[{}]创建失败,{}'.format(dbInfo['name'], result['msg'])
        self.state('databases', index, -1, err_msg)
        self.error(err_msg)
        return False

    def IsSqlError(self, mysqlMsg):
        """
        检测数据库执行错误
        :param mysqlMsg:
        :return:
        """
        mysqlMsg = str(mysqlMsg)
        if "MySQLdb" in mysqlMsg: return public.returnMsg(False, 'DATABASE_ERR_MYSQLDB')
        if "2002," in mysqlMsg or '2003,' in mysqlMsg: return public.returnMsg(False, 'DATABASE_ERR_CONNECT')
        if "using password:" in mysqlMsg: return public.returnMsg(False, 'DATABASE_ERR_PASS')
        if "Connection refused" in mysqlMsg: return public.returnMsg(False, 'DATABASE_ERR_CONNECT')
        if "1133" in mysqlMsg: return public.returnMsg(False, 'DATABASE_ERR_NOT_EXISTS')
        return None

    def map_to_list(self, map_obj):
        """
        map to list
        :param map_obj:
        :return:
        """
        try:
            if type(map_obj) != list and type(map_obj) != str: map_obj = list(map_obj)
            return map_obj
        except:
            return []

    def GetDatabaseAccess(self, name):
        """
        取数据库权限
        :param name: 数据库名称
        :return: 访问权限字符串
        """
        try:
            users = panelMysql.panelMysql().query(
                "select Host from mysql.user where User='" + name + "' AND Host!='localhost'")
            users = self.map_to_list(users)

            if len(users) < 1:
                return "127.0.0.1"

            accs = []
            for c in users:
                accs.append(c[0])
            userStr = ','.join(accs)
            return userStr
        except Exception as e:
            helpr.write_log_error(f"⚠️ 获取数据库 [{name}] 访问权限失败: {str(e)}，使用默认 127.0.0.1")
            return '127.0.0.1'

    def mypass(self, act, root):
        """
        数据库密码处理
        :param act:
        :param root:
        :return:
        """
        conf_file = '/etc/my.cnf'
        public.ExecShell("sed -i '/user=root/d' {}".format(conf_file))
        public.ExecShell("sed -i '/password=/d' {}".format(conf_file))
        if act:
            mycnf = public.readFile(conf_file)
            src_dump = "[mysqldump]\n"
            sub_dump = src_dump + "user=root\npassword=\"{}\"\n".format(root)
            if not mycnf: return False
            mycnf = mycnf.replace(src_dump, sub_dump)
            if len(mycnf) > 100: public.writeFile(conf_file, mycnf)
            return True
        return True

    def export_database(self, name, index):
        """
        导出数据库
        :param name:
        :param index:
        :return:
        """
        self.write_speed('done', '正在导出数据库')
        helpr.write_log("|-正在导出数据库{}...".format(name)),
        result = panelMysql.panelMysql().execute("show databases")
        isError = self.IsSqlError(result)
        if isError:
            err_msg = '数据库[{}]导出失败,{}!'.format(name, isError['msg'])
            self.state('databases', index, -1, err_msg)
            self.error(err_msg)
            return None

        root = public.M('config').where('id=?', (1,)).getField('mysql_root')
        backup_path = self._PLUGIN_PATH + '/backup'
        if not os.path.exists(backup_path): os.makedirs(backup_path, 384)
        self.mypass(True, root)
        backup_name = backup_path + '/psync_import.sql.gz'
        if os.path.exists(backup_name): os.remove(backup_name)
        public.ExecShell("/www/server/mysql/bin/mysqldump --default-character-set=" + public.get_database_character(
            name) + " --force --opt \"" + name + "\" | gzip > " + backup_name)
        self.mypass(False, root)
        if not os.path.exists(backup_name) or os.path.getsize(backup_name) < 30:
            if os.path.exists(backup_name): os.remove(backup_name)
            err_msg = '数据库[{}]导出失败!'.format(name)
            self.state('databases', index, -1, err_msg)
            self.error(err_msg)
            helpr.write_log("失败")
            return None
        helpr.write_log("成功")
        return backup_name

    def sync_ftp(self):
        """
        迁移FTP
        :return: None
        """
        total = len(self._SYNC_INFO['ftps'])
        success_count = 0
        fail_count = 0
        
        for i in range(total):
            try:
                self.state('ftps', i, 1)
                ftp = self._SYNC_INFO['ftps'][i]
                sp_msg = f"   📁 [{i+1}/{total}] 迁移FTP: {ftp['name']}"
                self.write_speed('action', sp_msg)
                helpr.write_log(sp_msg)
                
                ftp_id = ftp['id']
                ftpInfo = public.M('ftps').where('id=?', (ftp_id,)).field(
                    'id,name,password,path,status,ps').find()
                
                if not ftpInfo:
                    err_msg = f"❌ 指定FTP [{ftp['name']}] 不存在!"
                    self.state('ftps', i, -1, err_msg)
                    self.error(err_msg)
                    fail_count += 1
                    continue
                
                if self.send_ftp(ftpInfo, i):
                    self.state('ftps', i, 2)
                    success_count += 1
                    helpr.write_log(f"   ✅ FTP [{ftp['name']}] 同步成功")
                else:
                    fail_count += 1
                    helpr.write_log_error(f"   ❌ FTP [{ftp['name']}] 同步失败")
                    
                helpr.write_log("   " + "-" * 76)
                
            except Exception as e:
                helpr.write_log_error(f"   ❌ FTP [{ftp.get('name', 'Unknown')}] 同步异常: {str(e)}")
                helpr.write_log_error(f"   错误详情: {public.get_error_info()}")
                self.state('ftps', i, -1, str(e))
                fail_count += 1
        
        helpr.write_log(f"\n📊 FTP同步统计: 成功 {success_count} 个，失败 {fail_count} 个\n")

    def send_ftp(self, ftpInfo, index):
        """
        发送FTP
        :param ftpInfo:
        :param index:
        :return:
        """
        if not os.path.exists(ftpInfo['path']):
            self.state('ftps', index, -1, '指定FTP目录不存在{}!'.format(ftpInfo['path']))
            return False
        if not self.create_ftp(ftpInfo, index): return False
        if not ftpInfo['path'] in self._TO_PATHS:
            self._TO_PATHS.append(ftpInfo['path'])
            if not self.send(ftpInfo['path'], ftpInfo['path']):
                self.state('ftps', index, -1, '数据传输失败!')
                return False
        return True

    def create_ftp(self, ftpInfo, index):
        """
        创建远程FTP
        :param ftpInfo:
        :param index:
        :return:
        """
        pdata = {}
        pdata['ftp_username'] = ftpInfo['name']
        pdata['ftp_password'] = ftpInfo['password']
        pdata['path'] = ftpInfo['path']
        pdata['ps'] = ftpInfo['ps']

        result = self._API.send_panel('/ftp?action=AddUser', pdata)
        if not result['status']:
            err_msg = 'FTP帐户[{}]创建失败, {}'.format(ftpInfo['name'], result['msg'])
            self.state('ftps', index, -1, err_msg)
            self.error(err_msg)
            return False
        # 判断是否需要设置FTP状态
        if ftpInfo['status'] != '1':
            # 获取当前FTP标识
            result = self._API.send_panel('/data?action=getData',
                                          {"table": "ftps", "limit": 1, "p": 1, "order": "id desc"})
            if 'data' in result:
                if len(result['data']) > 0:
                    # 设置FTP状态
                    id = result['data'][0]['id']
                    self._API.send_panel('/ftp?action=SetStatus',
                                         {"id": id, "username": ftpInfo['name'], "status": '0'})
        return True

    def sync_crontab(self):
        """
        迁移计划任务
        :return: None
        """
        total = len(self._SYNC_INFO['crontab'])
        success_count = 0
        fail_count = 0
        
        for i in range(total):
            try:
                self.state('crontab', i, 1)
                cron = self._SYNC_INFO['crontab'][i]
                sp_msg = f"   ⏰ [{i+1}/{total}] 迁移计划任务: {cron['name']}"
                self.write_speed('action', sp_msg)
                helpr.write_log(sp_msg)
                
                cron_id = cron['id']
                crontabInfo = public.M('crontab').where('id=?', (cron_id,)).field(
                    'id,name,type,where1,where_hour,where_minute,echo,addtime,status,save,backupTo,sName,sBody,sType,urladdress,save_local,notice,notice_channel,db_type,split_type,split_value,type_id,rname,keyword,post_param,flock,time_set,backup_mode,db_backup_path,time_type,special_time,log_cut_path,user_agent,version,table_list,result,second,stop_site').find()
                
                if not crontabInfo:
                    err_msg = f"❌ 指定计划任务 [{cron['name']}] 不存在!"
                    self.state('crontab', i, -1, err_msg)
                    self.error(err_msg)
                    fail_count += 1
                    continue
                
                if self.send_crontab(crontabInfo, i):
                    self.state('crontab', i, 2)
                    success_count += 1
                    helpr.write_log(f"   ✅ 计划任务 [{cron['name']}] 同步成功")
                else:
                    fail_count += 1
                    helpr.write_log_error(f"   ❌ 计划任务 [{cron['name']}] 同步失败")
                    
                helpr.write_log("   " + "-" * 76)
                
            except Exception as e:
                helpr.write_log_error(f"   ❌ 计划任务 [{cron.get('name', 'Unknown')}] 同步异常: {str(e)}")
                helpr.write_log_error(f"   错误详情: {public.get_error_info()}")
                self.state('crontab', i, -1, str(e))
                fail_count += 1
        
        helpr.write_log(f"\n📊 计划任务同步统计: 成功 {success_count} 个，失败 {fail_count} 个\n")

    def send_crontab(self, crontabInfo, index):
        """
        发送计划任务
        :param crontabInfo:
        :param index:
        :return:
        """
        if not self.cancel_crontab(crontabInfo):
            if not self.create_crontab(crontabInfo, index): return False
        else:
            helpr.write_log("|-计划任务已存在,跳过")
        return True

    def create_crontab(self, crontabInfo, index):
        """
        创建远程计划任务
        :param crontabInfo:
        :param index:
        :return:
        """
        pdata = {}
        # 添加所有的计划任务信息
        pdata['name'] = crontabInfo['name']
        pdata['type'] = crontabInfo['type']
        pdata['where1'] = crontabInfo['where1']
        pdata['hour'] = crontabInfo['where_hour']
        pdata['minute'] = crontabInfo['where_minute']
        # pdata['echo'] = crontabInfo['echo']
        # pdata['addtime'] = crontabInfo['addtime']
        pdata['status'] = crontabInfo['status']
        pdata['save'] = crontabInfo['save']
        pdata['backupTo'] = crontabInfo['backupTo']
        pdata['sName'] = crontabInfo['sName']
        pdata['sBody'] = crontabInfo['sBody']
        pdata['sType'] = crontabInfo['sType']
        pdata['urladdress'] = crontabInfo['urladdress']
        pdata['save_local'] = crontabInfo['save_local']
        pdata['notice'] = crontabInfo['notice']
        pdata['notice_channel'] = crontabInfo['notice_channel']
        pdata['db_type'] = crontabInfo['db_type']
        pdata['split_type'] = crontabInfo['split_type']
        pdata['split_value'] = crontabInfo['split_value']
        # pdata['type_id'] = crontabInfo['type_id']
        pdata['rname'] = crontabInfo['rname']
        pdata['keyword'] = crontabInfo['keyword']
        pdata['post_param'] = crontabInfo['post_param']
        pdata['flock'] = crontabInfo['flock']
        pdata['time_set'] = crontabInfo['time_set']
        pdata['backup_mode'] = crontabInfo['backup_mode']
        pdata['db_backup_path'] = crontabInfo['db_backup_path']
        pdata['time_type'] = crontabInfo['time_type']
        pdata['special_time'] = crontabInfo['special_time']
        pdata['log_cut_path'] = crontabInfo['log_cut_path']
        pdata['user_agent'] = crontabInfo['user_agent']
        pdata['version'] = crontabInfo['version']
        pdata['table_list'] = crontabInfo['table_list']
        # pdata['result'] = crontabInfo['result']
        pdata['second'] = crontabInfo['second']
        pdata['stop_site'] = crontabInfo['stop_site']
        # print(pdata)
        result = self._API.send_panel('/crontab?action=AddCrontab', pdata)
        # 将结果转换为字典
        # print(result)
        # result = json.loads(result)
        if not result['status']:
            err_msg = '计划任务[{}]创建失败, {}'.format(crontabInfo['name'], result['msg'])
            self.state('crontab', index, -1, err_msg)
            self.error(err_msg)
            return False
        return True

    def cancel_crontab(self, crontabInfo):
        """
        检测远程计划任务是否存在
        :param name: 计划任务名称
        :return: 存在返回True
        """
        helpr.write_log("|-正在检测计划任务是否存在")
        result = self._API.send_panel('crontab?action=GetCrontab', {
            "p": 1,
            "count": 20,
            "search": crontabInfo['name'],
            "type_id": None,
            "order_param": None
        })
        if not result:
            return False
        if 'data' not in result:
            return False
        if len(result['data']) == 0:
            return False
        ven = ['name', 'sType', 'where_hour', 'where_minute']
        count = 0
        for i in result['data'][0]:
            if i in ven:
                if result['data'][0][i] == crontabInfo[i]:
                    count = count + 1
        if count == len(ven):
            helpr.write_log("|-计划任务已存在,跳过创建!")
            return True
        return False

    def sync_other(self):
        """
        迁移其他
        :return:
        """
        sp_msg = "|-正在迁移前置配置..."
        self.write_speed('action', sp_msg)
        self.write_speed('done', '正在传输文件')
        helpr.write_log(sp_msg)
        helpr.write_log("=" * 50)
        s_files = [
            ["/www/server/pass", "密码文件"],
            ["/www/server/vhost/letsencrypt", "lets证书"],
            ["/www/server/panel/config/letsencrypt.json", "lets帐户信息"],
            ["/www/server/panel/config/letsencrypt_v2.json", "lets帐户信息v2"],
            ["/www/server/panel/vhost/nginx/0.monitor_log_format.conf", "监控报表"],
        ]
        self.send_list(s_files)
        helpr.write_log("=" * 50)

    def set_mode(self, filename, mode):
        """
        设置文件权限
        :param filename:
        :param mode:
        :return:
        """
        if not os.path.exists(filename): return False
        mode = int(str(mode), 8)
        os.chmod(filename, mode)
        return True

    def send(self, spath, dpath, force=False):
        """
        发送文件
        :param spath:
        :param dpath:
        :param force:
        :return:
        """
        if not os.path.isdir(spath):
            return self._API.upload_file(spath, dpath, True)

        # 创建目录
        self._API.send_panel('/files?action=CreateDir', {"path": dpath})

        # 是否压缩目录后上传
        if self._SYNC_INFO['zip'] and not force:
            backup_path = self._PLUGIN_PATH + '/backup'
            if not os.path.exists(backup_path): os.makedirs(backup_path, 384)
            zip_file = backup_path + "/psync_tmp_{}.tar.gz".format(os.path.basename(spath))
            zip_dst = '/www/server/panel/temp/psync_tmp_{}.tar.gz'.format(os.path.basename(dpath))
            if not os.path.exists(zip_file) or not self.read_log("|-正在压缩目录[{}]...".format(spath)):
                helpr.write_log("|-正在压缩目录[{}]...".format(spath))
                self.write_speed('done', '正在压缩')
                public.ExecShell("cd {} && tar zcvf {} ./ > /dev/null".format(spath, zip_file))
                if not os.path.exists(zip_file):
                    self.error("目录[{}]打包失败!".format(spath))
                    return False
            self.set_mode(zip_file, 600)
            if not self._API.upload_file(zip_file, zip_dst, True):
                self.error("目录[{}]上传失败!".format(spath))
                if os.path.exists(zip_file): os.remove(zip_file)
                return False

            if os.path.exists(zip_file): os.remove(zip_file)
            helpr.write_log("|-正在解压文件到目录[{}]...".format(dpath))
            self.write_speed('done', '正在解压')
            self._API.send_panel('/files?action=UnZip',
                                 {"sfile": zip_dst, "dfile": dpath, "type": "tar", "coding": "UTF-8",
                                  "password": "undefined"})
            n = 0
            while True:
                time.sleep(3)
                result = self._API.send_panel('/task?action=get_task_lists', {"status": '-1'})
                if not result or n > 20: break
                n += 1

            self._API.send_panel('/files?action=ExecShell', {"shell": "rm -f " + zip_dst, "path": "/www"}, 30)
            helpr.write_log("|-解压完成!")
            return True

        # 遍历上传
        for name in os.listdir(spath):
            sfile = os.path.join(spath, name)
            dfile = os.path.join(dpath, name)
            self.send(sfile, dfile)
        return True

    def read_log(self, tmp):
        """
        读旧日志判断是不是压缩完了
        :param tmp:
        :return:
        """
        log_path = '/www/server/panel/logs/old_psync.log'
        log = public.readFile(log_path)
        if not log:
            return False
        logs = log.split('\n==================================================\n')
        for i in logs:
            if tmp in i and tmp != i.split("\n|-"):
                return True
        return False

    def write_speed(self, key, value):
        """
        写进度信息到文件
        :param key: 进度键名
        :param value: 进度值
        :return: None
        """
        try:
            # 默认进度信息
            default_speed = {
                "time": int(time.time()),
                "size": 0,
                "used": 0,
                "total_size": 0,
                "speed": 0,
                "action": "等待中",
                "done": "等待中",
                "end_time": int(time.time())
            }
            
            # 读取现有进度信息
            if os.path.exists(self._SPEED_FILE):
                try:
                    file_content = public.readFile(self._SPEED_FILE)
                    speed_info = json.loads(file_content) if file_content else default_speed
                except json.JSONDecodeError:
                    helpr.write_log_error("⚠️ 进度文件格式错误，重新初始化")
                    speed_info = default_speed
            else:
                speed_info = default_speed
            
            # 更新进度值
            if key not in speed_info:
                speed_info[key] = 0
            
            if key == 'total_size':
                speed_info[key] += value  # 累加
            else:
                speed_info[key] = value  # 覆盖
            
            # 写入文件
            public.writeFile(self._SPEED_FILE, json.dumps(speed_info, ensure_ascii=False, indent=2))
            
        except Exception as e:
            helpr.write_log_error(f"⚠️ 写入进度信息失败: {str(e)}")

    def check_env(self) -> bool:
        """
        检查环境并安装必要软件
        :return:
        """
        helpr.write_log(f"|-正在检查目标服务器'{self._SYNC_CONFIG['title']}'环境...")
        src_env = self._API.get_src_config()
        dst_env = self._API.get_dst_config()

        if not src_env or not dst_env:
            return False

        if self._API.get_dst_task_speed():
            helpr.write_log(f"|-目标服务器'{self._SYNC_CONFIG['title']}'正在执行任务，请稍后再试!")
            return False

        # 网站环境检测
        if src_env['webserver'] != dst_env['webserver'] and src_env['webserver'] != '未安装':
            helpr.write_log(f"|-目标服务器'{self._SYNC_CONFIG['title']}'网站环境不匹配，正在发起安装...")
            webserver_version = "1.24"
            min_version = "62"
            if src_env['webserver'] == 'nginx':
                webserver_version = "1.24"
                min_version = 0
            if src_env['webserver'] == 'apache':
                webserver_version = "2.4"
                min_version = 62
            if src_env['webserver'] == 'openlitespeed':
                webserver_version = "1.7"
                min_version = 17
            if not self._API.dst_install_plugin(src_env['webserver'], webserver_version, min_version):
                helpr.write_log(f"|-服务器'{self._SYNC_CONFIG['title']}'网站环境安装失败，请检查网站环境是否正确!")
                return False
            helpr.write_log(f"|-服务器'{self._SYNC_CONFIG['title']}'网站环境【{src_env['webserver']}】发起安装成功！")

        else:
            helpr.write_log(f"|-服务器'{self._SYNC_CONFIG['title']}'网站环境检测完成！")
        #  数据库环境检测
        if src_env['mysql'] and src_env['mysql'] != dst_env['mysql']:
            helpr.write_log(f"|-目标服务器'{self._SYNC_CONFIG['title']}'数据库环境不匹配，正在发起安装...")
            if not self._API.dst_install_plugin('mysql', '5.7', 44):
                helpr.write_log(f"|-服务器'{self._SYNC_CONFIG['title']}'数据库环境安装失败，请检查数据库环境是否正确!")
                return False
            helpr.write_log(f"服务器'{self._SYNC_CONFIG['title']}'数据库环境【mysql】发起安装成功！")
        else:
            helpr.write_log(f"|-服务器'{self._SYNC_CONFIG['title']}'数据库环境检测成功！")

        # FTP环境检测
        if src_env['ftp'] and src_env['ftp'] != dst_env['ftp']:
            helpr.write_log(f"|-目标服务器'{self._SYNC_CONFIG['title']}'FTP环境不匹配，正在发起安装...")
            if not self._API.dst_install_plugin('pureftpd', '1.0', 49):
                helpr.write_log(f"|-服务器'{self._SYNC_CONFIG['title']}'FTP环境安装失败，请检查FTP环境是否正确!")
                return False
            helpr.write_log(f"服务器'{self._SYNC_CONFIG['title']}'FTP环境【pureftpd】发起安装成功！")
        else:
            helpr.write_log(f"|-服务器'{self._SYNC_CONFIG['title']}'FTP环境检测完成！")

        php_min_versions = {
            '52': "17",
            '53': "29",
            '54': "45",
            '55': "38",
            '56': "40",
            '70': "33",
            '71': "33",
            '72': "33",
            '73': "32",
            '74': "33",
            '80': "26",
            '81': "31",
            '82': "27",
            '83': "15",
            '84': "2",
        }
        # php环境检测 src_env['php'] 和 dst_env['php']分别都是数组，例如：['73','74']，对比出差异的数组进行遍历安装
        if len(src_env['php']) > 0 and src_env['php'] != dst_env['php']:
            helpr.write_log(f"|-目标服务器'{self._SYNC_CONFIG['title']}'PHP环境不匹配，正在发起安装...")
            for php_version in src_env['php']:
                if not php_version in dst_env['php']:
                    php_min_version = php_min_versions[php_version] or 0
                    # php_version的格式是54、73、74，需要按字符拆分成5.4、7.3、7.4进行安装
                    php_version = php_version[0] + "." + php_version[1:]
                    if not self._API.dst_install_plugin('php-' + php_version, php_version, php_min_version):
                        helpr.write_log(f"|-服务器'{self._SYNC_CONFIG['title']}'PHP环境安装失败，请检查PHP环境是否正确!")
                        return False
                    helpr.write_log(f"|-服务器'{self._SYNC_CONFIG['title']}'PHP环境【php-{php_version}】发起安装成功！")
                    # 发起扩展检测并安装,需要去掉php_version中的.
                    php_version = php_version.replace(".", "")
                    self.check_php_ext(php_version)
        else:
            helpr.write_log(f"|-服务器'{self._SYNC_CONFIG['title']}'PHP环境检测完成！")

        helpr.write_log(f"|-服务器'{self._SYNC_CONFIG['title']}'环境检测完成！")
        return True


    def check_php_ext(self, version: str) -> bool:
        """
        检查PHP扩展
        :param version:
        :return:
        """
        helpr.write_log(f"|-正在检查目标服务器'{self._SYNC_CONFIG['title']}'PHP扩展...")
        src_php_ext = helpr.get_php_config(version)
        if not src_php_ext:
            helpr.write_log_error(f"|-服务器'{self._SYNC_CONFIG['title']}'PHP扩展获取失败，请检查PHP扩展是否正确!")
            return False
        if not src_php_ext:
            return False
        for libs in src_php_ext['libs']:
            if libs['status']:
                if not self._API.install_php_ext(libs['name'], version):
                    helpr.write_log_error(
                        f"|-服务器'{self._SYNC_CONFIG['title']}'PHP[{libs['name']}]扩展安装失败，请检查PHP扩展是否正确!")
                    return False
                helpr.write_log(f"|-服务器'{self._SYNC_CONFIG['title']}'PHP[{libs['name']}]扩展发起安装成功！")

        helpr.write_log(f"|-服务器'{self._SYNC_CONFIG['title']}'PHP扩展检测完成！")
        return True


    def check_php_disable_functions(self) -> bool:
        """
        检查PHP禁用函数
        :param version:
        :return:
        """
        helpr.write_log(f"|-正在检查目标服务器'{self._SYNC_CONFIG['title']}'PHP禁用函数...")
        src_env = self._API.get_src_config()
        for php_version in src_env['php']:
            src_php_config = helpr.get_php_config(php_version)
            dst_php_config = self._API.get_php_config(php_version)
            if not src_php_config or not dst_php_config:
                # 跳过，继续循环下一个
                continue
            if src_php_config['disable_functions'] != dst_php_config['disable_functions']:
                helpr.write_log(
                    f"|-服务器'{self._SYNC_CONFIG['title']}'PHP[{php_version}]禁用函数不匹配，正在发起修改...")
                if not self._API.set_php_disable_functions(php_version, src_php_config['disable_functions']):
                    helpr.write_log_error(f"服务器'{self._SYNC_CONFIG['title']}'PHP[{php_version}]禁用函数修改失败，请检查PHP禁用函数是否正确!")

        helpr.write_log(f"|-服务器'{self._SYNC_CONFIG['title']}'PHP禁用函数检测完成！")
        return True